package org.new9uy.tetris.model;

import java.awt.Color;
import java.util.Random;

import org.new9uy.tetris.GameConstants;
import org.new9uy.tetris.util.ArrayUtils;
import org.new9uy.tetris.model.tetrimino.*;


public class Matrix
{
    private boolean active;
    private boolean over = false;
    private Mino[][] mtx;
    protected Tetrimino current;
    private Tetrimino botMind;
    private boolean lignes;
    private boolean ghostOn;
    private boolean botMindOn;
    private boolean next_has_spawned = false;
    private int spawnedlines = 0;
    private int x_gap = 0;

    public Matrix(int x, int y)
    {
        lignes = false;
        ghostOn = false;
        botMindOn = true;

        mtx = new Mino[x + 2][y + 2];

        //Initialize invisible border minoes:
        for (int i = 0; i < x + 2; i++)
        {
            mtx[i][0] = new Mino(i, 0, true);
            mtx[i][y + 1] = new Mino(i, y + 1, true);
        }
        for (int i = 0; i < y + 1; i++)
        {
            mtx[0][i] = new Mino(0, i, true);
            mtx[x + 1][i] = new Mino(x + 1, i, true);
        }


        //Initialize visible minoes:
        for (int i = 1; i < x + 1; i++)
        {
            for (int j = 1; j < y + 1; j++)
            {
                mtx[i][j] = new Mino(i, j);
            }
        }

        botMind = null;
    }

    public boolean Lines()
    {
        if (lignes)
        {
            lignes = false;
            return true;
        } else
        {
            return false;
        }
    }
    //Used by bot to know when to calc the next piece

    public boolean nextHasSpawned()
    {
        if (next_has_spawned)
        {
            next_has_spawned = false;
            return true;
        }

        return false;
    }

    public int getXDim()
    {
        return mtx.length - 2;
    }

    public int getYDim()
    {
        return mtx[0].length - 2;
    }

    public Mino getMino(int x, int y)
    {
        return mtx[x][y];
    }

    public void setMino(int x, int y, Mino mino)
    {
        mtx[x][y] = mino;
    }

    public boolean isActive()
    {
        return active;
    }

    public void setActive(boolean active)
    {
        this.active = active;
    }

    public boolean isOver()
    {
        return over;
    }

    public void setLines()
    {
        lignes = true;
    }

    public boolean isGhostOn()
    {
        return ghostOn;
    }

    public boolean isBotMindOn()
    {
        return botMindOn;
    }

    public void setBotMindOn(boolean on)
    {
        botMindOn = on;
    }

    public Tetrimino getBotMind()
    {
        return botMind;
    }

    public void reset()
    {
        for (int i = 1; i <= getXDim(); i++)
        {
            for (int j = 1; j <= getYDim(); j++)
            {
                mtx[i][j].setColor(GameConstants.MINO_EMPTY_COLOR);
                mtx[i][j].setEmpty(true);
            }
        }
        over = false;
        spawnedlines = 0;
        lignes = false;
        //botMindOn = false;
    }

    //-------------------------------------------------------------------
    // PRIVATE DRAWING METHODS :
    //-------------------------------------------------------------------
    private void drawTetrimino(Tetrimino tet)
    {
        changeColors(tet, tet.getColor());
    }

    private void undrawTetrimino(Tetrimino tet)
    {
        changeColors(tet, GameConstants.MINO_EMPTY_COLOR);
    }

    private void changeColors(Tetrimino tet, Color color)
    {
        for (int i = 0; i < 4; i++)
        {
            tet.mino(i).setColor(color);
        }
    }

    private void refresh(Mino[] mino)
    {
        undrawTetrimino(current);

        current.setTetrimino(mino);

        drawTetrimino(current);
    }

    private void toBottom(Mino[] mino)
    {
        boolean loop = true;
        while (loop)
        {
            for (int i = 0; i < mino.length; i++)
            {
                mino[i] = transposeMino(mino[i], -1, 'y');
            }

            if (!availableHere(mino))
            {
                for (int i = 0; i < mino.length; i++)
                {
                    mino[i] = transposeMino(mino[i], 1, 'y');
                }
                loop = false;
            }
        }
    }

    //-------------------------------------------------------------------
    // BOT CALCULATION METHODS : (For bot's use only
    //-------------------------------------------------------------------
    public int currentOrientation()
    {
        return current.getOrientation();
    }

    public int currentLeftX()
    {
        return current.mino(current.searchMino("MinX")).getX();
    }

    public void botReset() //Place the botMind tetrimino to the very left
    {
        int x = botMind.mino(botMind.searchMino("MinX")).getX();
        Mino[] m = botMind.getTetrimino();
        for (int i = 0; i < 4; i++)
        {
            m[i] = transposeMino(m[i], 1 - x, 'x');
        }
    }

    public void botMove() //Places the botMind tetrimino 1 step to the right
    {
        Mino[] m = botMind.getTetrimino();
        for (int i = 0; i < 4; i++)
        {
            m[i] = transposeMino(m[i], 1, 'x');
        }
    }

    public void botRotate()
    {
        Mino[] m = botMind.getTetrimino();
        for (int i = 0; i < 4; i++)
        {
            m[i] = transposeMino(m[i], -2, 'x');
        }
        botMind.setTetrimino(m);
        botMind.setTetrimino(botMind.rotate(this));
        botMind.updateOrientation();////////////////////////////////////
        botReset();
    }

    public int countWholesBelow(Tetrimino t)
    {
        return countWholesBelow(t.lowests());
    }

    public int countWholesBelow(Mino[] m)
    {
        int n = 0;
        for (int i = 0; i < m.length; i++)
        {
            n += countWholesBelow(m[i]);
        }
        return n;
    }

    public int countWholesBelow(Mino m)
    {
        int n = 0;
        m = transposeMino(m, -1, 'y');

        while (m.isEmpty())
        {
            m = transposeMino(m, -1, 'y');
            n++;
        }
        return n;
    }

    //-------------------------------------------------------------------
    // DISPLACEMENT METHODS :
    //-------------------------------------------------------------------
    private Mino transposeMino(Mino mino, int amount, char dir)
    {
        int x = mino.getX(), y = mino.getY();
        switch (dir)
        {
            case 'x':
                return mtx[x + amount][y];
            case 'y':
                return mtx[x][y + amount];
            default:
                throw new IllegalArgumentException(
                        "The value " + dir + " for the argument dir is "
                        + "cannot be recognised.");
        }
    }
    /*
     * dir ::= 'x' | 'y'
     * @return true if it was possible to move the tetrimino.
     *         false is no changes were applied.
     */

    public synchronized boolean moveTetrimino(char dir)
    {
        return moveTetrimino(dir, 1);
    }

    public synchronized boolean moveTetrimino(char dir, int amount)
    {
        boolean ret = generalMove(dir, amount, current);
        return ret;
    }

    private boolean generalMove(char dir, int amount, Tetrimino tet)
    {
        if (over)
        {
            return false;
        }
        Mino[] mino = ArrayUtils.duplicate(tet.getTetrimino());

        //move tetrimino
        for (int i = 0; i < 4; i++)
        {
            mino[i] = transposeMino(mino[i], amount, dir);
            //check availability.
            if (!mino[i].isEmpty())
            {
                return false;
            }
        }
        refresh(mino);

        return true;
    }

    public synchronized void rotateTetrimino()
    {
        if (over)
        {
            return;
        }
        Mino[] mino = null;
        try
        {
            mino = current.rotate(this);
        } catch (ArrayIndexOutOfBoundsException e)
        {
            //e.printStackTrace();
            return;
        }

        if (availableHere(mino))
        {
            current.updateOrientation();
            refresh(mino);
        }
    }

    public synchronized void dropTetrimino()
    {
        if (over)
        {
            return;
        }
        Mino[] mino = ArrayUtils.duplicate(current.getTetrimino());
        toBottom(mino);
        refresh(mino);
    }

    //-------------------------------------------------------------------
    // MINO-PHYSICS METHODS :
    //-------------------------------------------------------------------
    //returns true if it is possible to draw this tetrimino here.
    //        false otherwise.
    public boolean availableHere(Tetrimino tet)
    {
        return availableHere(tet.getTetrimino());
    }

    public boolean availableHere(Mino[] mino)
    {
        for (int i = 0; i < mino.length; i++)
        {
            if (!mino[i].isEmpty())
            {
                return false;
            }
        }
        return true;
    }

    public void spawnTetrimino(Tetrimino tet)
    {
        current = tet;

        if (!availableHere(current))
        {
            gameOver();
        } else
        {
            drawTetrimino(current);
            if (botMindOn)
            {
                botMind = (Tetrimino)current.clone();
            }
            next_has_spawned = true;
        }
    }

    protected void lockCurrent()
    {
        for (int i = 0; i < 4; i++)
        {
            current.mino(i).setEmpty(false);
        }
    }

    //-------------------------------------------------------------------
    // GAME OVER METHODS :
    //-------------------------------------------------------------------
    public synchronized void gameOver()
    {
        if (over)
        {
            return;
        }
        over = true;
        gameOverAnimation();
        spellDefaultDeath();
    }

    public synchronized void left()
    {
        if (over)
        {
            return;
        }
        over = true;
        gameOverAnimation();
        spellLeft(GameConstants.MINO_EMPTY_COLOR);
    }

    private void gameOverAnimation()
    {
        for (int i = 0; i <= getYDim(); i++)
        {
            for (int j = 0; j <= getXDim(); j++)
            {
                try
                {
                    mtx[j][i].setColor(Color.RED);
                    Thread.sleep(1);
                } catch (Exception ie)
                {
                }
            }
        }
    }

    public synchronized void spellDefaultDeath()
    {
        spellSkullImage(Color.WHITE, Color.RED, Color.GRAY, 2);
        spellKO(Color.BLACK, 13);
    }

    public synchronized void spellLeft(Color c)
    {
        spellLeft(c, 8);
    }

    public synchronized void spellLeft(Color c, int y)
    {
        if (y < 1 || y > 14)
        {
            throw new IllegalArgumentException("y must be between 1 and 14");
        }
        //L
        mtx[1][y + 4].setColor(c);
        mtx[1][y + 3].setColor(c);
        mtx[1][y + 2].setColor(c);
        mtx[1][y + 1].setColor(c);
        mtx[1][ y].setColor(c);
        mtx[2][y].setColor(c);
        //E
        mtx[3][y + 6].setColor(c);
        mtx[4][y + 6].setColor(c);
        mtx[3][y + 5].setColor(c);
        mtx[3][y + 4].setColor(c);
        mtx[4][y + 4].setColor(c);
        mtx[3][y + 3].setColor(c);
        mtx[3][y + 2].setColor(c);
        mtx[4][y + 2].setColor(c);
        //F
        mtx[6][y + 4].setColor(c);
        mtx[7][y + 4].setColor(c);
        mtx[6][y + 3].setColor(c);
        mtx[6][y + 2].setColor(c);
        mtx[7][y + 2].setColor(c);
        mtx[6][y + 1].setColor(c);
        mtx[6][ y].setColor(c);
        //T
        mtx[8][y + 6].setColor(c);
        mtx[9][y + 6].setColor(c);
        mtx[10][y + 6].setColor(c);
        mtx[9][y + 5].setColor(c);
        mtx[9][y + 4].setColor(c);
        mtx[9][y + 3].setColor(c);
        mtx[9][y + 2].setColor(c);
    }

    public synchronized void spellTheEnd(Color color)
    {
        //T:
        mtx[1][16].setColor(color);
        mtx[2][16].setColor(color);
        mtx[3][16].setColor(color);
        mtx[2][15].setColor(color);
        mtx[2][14].setColor(color);
        mtx[2][13].setColor(color);
        mtx[2][12].setColor(color);
        //H:
        mtx[5][16].setColor(color);
        mtx[7][16].setColor(color);
        mtx[5][15].setColor(color);
        mtx[7][15].setColor(color);
        mtx[5][14].setColor(color);
        mtx[7][14].setColor(color);
        mtx[5][13].setColor(color);
        mtx[7][13].setColor(color);
        mtx[5][12].setColor(color);
        mtx[7][12].setColor(color);
        mtx[6][14].setColor(color);
        //E:
        mtx[9][16].setColor(color);
        mtx[10][16].setColor(color);
        mtx[9][15].setColor(color);
        mtx[9][14].setColor(color);
        mtx[10][14].setColor(color);
        mtx[9][13].setColor(color);
        mtx[9][12].setColor(color);
        mtx[10][12].setColor(color);
        //E:
        mtx[1][9].setColor(color);
        mtx[2][9].setColor(color);
        mtx[1][8].setColor(color);
        mtx[1][7].setColor(color);
        mtx[2][7].setColor(color);
        mtx[1][6].setColor(color);
        mtx[1][5].setColor(color);
        mtx[2][5].setColor(color);
        //N: (miniscule)
        mtx[4][9].setColor(color);
        mtx[6][9].setColor(color);
        mtx[4][8].setColor(color);
        mtx[6][8].setColor(color);
        mtx[4][7].setColor(color);
        mtx[6][7].setColor(color);
        mtx[4][6].setColor(color);
        mtx[6][6].setColor(color);
        mtx[4][5].setColor(color);
        mtx[6][5].setColor(color);
        mtx[5][9].setColor(color);
        //D: (miniscule)
        mtx[10][9].setColor(color);
        mtx[10][8].setColor(color);
        mtx[10][7].setColor(color);
        mtx[9][7].setColor(color);
        mtx[8][7].setColor(color);
        mtx[10][6].setColor(color);
        mtx[8][6].setColor(color);
        mtx[10][5].setColor(color);
        mtx[9][5].setColor(color);
        mtx[8][5].setColor(color);
    }

    public synchronized void spellKO(Color color, int y)
    {
        if (y < 1 || y > 16)
        {
            throw new IllegalArgumentException("y must be between 1 and 16");
        }
        //K:
        mtx[1][ y].setColor(color);
        mtx[4][ y].setColor(color);
        mtx[1][y + 1].setColor(color);
        mtx[3][y + 1].setColor(color);
        mtx[1][y + 2].setColor(color);
        mtx[2][y + 2].setColor(color);
        mtx[1][y + 3].setColor(color);
        mtx[3][y + 3].setColor(color);
        mtx[1][y + 4].setColor(color);
        mtx[4][y + 4].setColor(color);
        //O:
        mtx[7][y + 1].setColor(color);
        mtx[10][y + 1].setColor(color);
        mtx[7][y + 2].setColor(color);
        mtx[10][y + 2].setColor(color);
        mtx[7][y + 3].setColor(color);
        mtx[10][y + 3].setColor(color);

        mtx[8][y].setColor(color);
        mtx[9][y].setColor(color);
        mtx[8][y + 4].setColor(color);
        mtx[9][y + 4].setColor(color);
    }

    public synchronized void spellSkullImage(Color bone, Color eye, Color teeth, int y)
    {
        if (y < 1 || y > 12)
        {
            throw new IllegalArgumentException("y must be between 1 and 12");
        }
        //HEAD:
        mtx[4][y + 8].setColor(bone);
        mtx[5][y + 8].setColor(bone);
        mtx[6][y + 8].setColor(bone);
        mtx[7][y + 8].setColor(bone);
        mtx[3][y + 7].setColor(bone);
        mtx[4][y + 7].setColor(bone);
        mtx[5][y + 7].setColor(bone);
        mtx[6][y + 7].setColor(bone);
        mtx[7][y + 7].setColor(bone);
        mtx[8][y + 7].setColor(bone);

        mtx[3][y + 6].setColor(bone);
        mtx[5][y + 6].setColor(bone);
        mtx[6][y + 6].setColor(bone);
        mtx[8][y + 6].setColor(bone);
        mtx[3][y + 5].setColor(bone);
        mtx[4][y + 5].setColor(bone);
        mtx[5][y + 5].setColor(bone);
        mtx[6][y + 5].setColor(bone);
        mtx[7][y + 5].setColor(bone);
        mtx[8][y + 5].setColor(bone);

        //EYES:
        mtx[4][y + 6].setColor(eye);
        mtx[7][y + 6].setColor(eye);

        //TEETH:
        mtx[5][y + 4].setColor(teeth);
        mtx[6][y + 4].setColor(teeth);
        mtx[5][y + 3].setColor(teeth);
        mtx[6][y + 3].setColor(teeth);

        //ARMS:
        mtx[2][y + 4].setColor(bone);
        mtx[9][y + 4].setColor(bone);
        mtx[3][y + 3].setColor(bone);
        mtx[8][y + 3].setColor(bone);
        mtx[4][y + 2].setColor(bone);
        mtx[5][y + 2].setColor(bone);
        mtx[6][y + 2].setColor(bone);
        mtx[7][y + 2].setColor(bone);
        mtx[3][y + 1].setColor(bone);
        mtx[8][y + 1].setColor(bone);
        mtx[2][ y].setColor(bone);
        mtx[9][ y].setColor(bone);

    }

    //-------------------------------------------------------------------
    // COLUMN-CONTROL METHODS :
    //-------------------------------------------------------------------
    public int[] getColumns(Tetrimino tet)
    {
        //	GET COLS :
        int[] cols = new int[4];
        for (int i = 0; i < 4; i++)
        {
            cols[i] = tet.mino(i).getX();
        }

        cols = ArrayUtils.noDups(cols);
        ArrayUtils.sortDescending(cols);
        return cols;
    }

    //-------------------------------------------------------------------
    // LINE-CONTROL METHODS :
    //-------------------------------------------------------------------
    public int[] getLines(Tetrimino tet)
    {
        //	GET LINES :
        int[] lines = new int[4];
        for (int i = 0; i < 4; i++)
        {
            lines[i] = tet.mino(i).getY();
        }

        lines = ArrayUtils.noDups(lines);
        ArrayUtils.sortDescending(lines);
        return lines;
    }

    public synchronized void clearLines(int[] lines)
    {
        clign(lines);
        clign(lines);
        checkClearSpawnedLines(lines);
        for (int i = 0; i < lines.length; i++)
        {
            if (lines[i] != -1)
            {
                shiftDown(lines[i]);
            }
        }
    }

    private void checkClearSpawnedLines(int[] lines)
    {
        int toclear = 0;
        for (int i = 0; i < lines.length; i++)
        {
            if (lines[i] != -1 && lines[i] <= spawnedlines)
            {
                toclear++;
            }
        }
        spawnedlines -= toclear;
    }

    public synchronized void takeLines(int num)
    {
        if (over)
        {
            return;
        }
        boolean ovrflw1, ovrflw2;
        shiftCurrentUp(num);
        ovrflw1 = shiftUp(num);
        ovrflw2 = !availableHere(current);
        refresh(current.getTetrimino());

        if (ovrflw1 || ovrflw2)
        {
            gameOver();
            return;//No use to draw the lines	
        }
        spawnLineAttack(num);
        refresh(current.getTetrimino());
    }

    private void spawnLineAttack(int numlines)
    {
        //Place a gap
        if (spawnedlines == 0)
        {
            Random r = new Random();
            x_gap = r.nextInt(10) + 1;
        }

        //Draw attacking lines
        for (int y = 1; y <= numlines; y++)
        {
            for (int x = 1; x < 11; x++)
            {
                if (x != x_gap)
                {
                    mtx[x][y].setEmpty(false);
                    mtx[x][y].setColor(GameConstants.MINO_LINESPAWN_COLOR);
                } else
                {
                    mtx[x][y].setEmpty(true);
                    mtx[x][y].setColor(GameConstants.MINO_EMPTY_COLOR);
                }
            }
        }
        spawnedlines += numlines;
    }

    private void shiftDown(int gapline)
    {
        //System.out.print("\nremoving line: "+gapline);
        for (int y = gapline; y <= getYDim(); y++)
        {
            for (int x = 1; x <= getXDim(); x++)
            {
                above(x, y);
            }
        }
        //TOP LINE:
        for (int x = 1; x <= getXDim(); x++)
        {
            mtx[x][getYDim()].setEmpty(true);
            mtx[x][getYDim()].setColor(GameConstants.MINO_EMPTY_COLOR);
        }
    }
    //@return true if overflow

    private boolean shiftUp(int num)
    {
        boolean ovrflw = false;
        //CHECK OVRFLOW :
        for (int x = 1; x < 11; x++)
        {
            for (int y = 20; y > 20 - num; y--)
            {
                if (!mtx[x][y].isEmpty())
                {
                    ovrflw = true;
                }
            }
        }

        //SHIFT LINES
        for (int y = 20; y > num; y--)
        {
            for (int x = 1; x < 11; x++)
            {
                below(x, y, num);
            }
        }

        return ovrflw;
    }
    //@return true if overflow

    private boolean shiftCurrentUp(int num)
    {
        //Shift the current tetrimino up so that it doesn't collide
        //into the rising minoes.
        try
        {
            if (!generalMove('y', num, current))
            {
                throw new ArrayIndexOutOfBoundsException("hit line 21");
            }
        } //If a line attack causes the current Tetrimino to overflow 
        //then try place it at the very top:
        catch (ArrayIndexOutOfBoundsException e)
        {
            //e.printStackTrace();
            int maxy = current.mino(current.searchMino("MaxY")).getY();
            if (!generalMove('y', 20 - maxy, current))
            {
                return true; //Impossible to move the Tetrimino up. [Causes ovrflw]
            }
        }
        return false;
    }

    private void above(int x, int y)
    {
        boolean empty = mtx[x][y + 1].isEmpty();
        mtx[x][y].setEmpty(empty);

        if (empty)
        {
            mtx[x][y].setColor(GameConstants.MINO_EMPTY_COLOR);
        } else
        {
            mtx[x][y].setColor(mtx[x][y + 1].getColor());
        }
    }

    private void below(int x, int y, int jump)
    {
        boolean empty = mtx[x][y - jump].isEmpty();
        mtx[x][y].setEmpty(empty);

        if (empty)
        {
            mtx[x][y].setColor(GameConstants.MINO_EMPTY_COLOR);
        } else
        {
            mtx[x][y].setColor(mtx[x][y - jump].getColor());
        }
    }

    private void lightLine(int gapline)
    {
        for (int i = 0; i <= getXDim(); i++)
        {
            mtx[i][gapline].setColor(GameConstants.MINO_BLINK_COLOR);
        }
    }

    private void blackLine(int gapline)
    {
        for (int i = 0; i <= getXDim(); i++)
        {
            mtx[i][gapline].setColor(GameConstants.MINO_EMPTY_COLOR);
        }
    }

    private void clign(int[] lines)
    {
        try
        {
            for (int i = 0; i < lines.length; i++)
            {
                if (lines[i] != -1)
                {
                    lightLine(lines[i]);
                }
            }
            Thread.sleep(200);
            for (int i = 0; i < lines.length; i++)
            {
                if (lines[i] != -1)
                {
                    blackLine(lines[i]);
                }
            }
            Thread.sleep(200);
        } catch (Exception ie)
        {
        }
    }
}
